/*
 * Copyright (c) 1998, 2025 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 1998, 2024 IBM Corporation. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     11/07/2017 - Dalia Abo Sheasha
//       - 526957 : Split the logging and trace messages
package org.eclipse.persistence.logging;

import org.eclipse.persistence.internal.localization.LoggingLocalization;
import org.eclipse.persistence.internal.localization.TraceLocalization;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.function.Supplier;

/**
 * Represents the abstract log that implements all the generic logging functions.
 * It contains a singleton SessionLog that logs messages from outside any EclipseLink session.
 * The singleton SessionLog can also be passed to an EclipseLink session when messages
 * are logged through that session. By default, a singleton {@linkplain DefaultSessionLog} is created.
 *
 * @see SessionLog
 * @see SessionLogEntry
 * @see DefaultSessionLog
 */
public abstract class AbstractSessionLog implements SessionLog, java.lang.Cloneable {

    // Copy of PersistenceUnitProperties.LOGGING_LEVEL to break dependency
    private static final String LOGGING_LEVEL = "eclipselink.logging.level";

    /**
     * Represents the log level
     */
    protected int level;

    /**
     * Represents the singleton SessionLog
     */
    protected static SessionLog defaultLog;

    private String sessionName;

    /**
     * Represents prefix to logged severe
     */
    protected static String SEVERE_PREFIX = null;

    /**
     * Represents prefix to logged warning
     */
    protected static String WARNING_PREFIX = null;

    /**
     * Represents prefix to logged info
     */
    protected static String INFO_PREFIX = null;

    /**
     * Represents prefix to logged config
     */
    protected static String CONFIG_PREFIX = null;

    /**
     * Represents prefix to logged fine
     */
    protected static String FINE_PREFIX = null;

    /**
     * Represents prefix to logged finer
     */
    protected static String FINER_PREFIX = null;

    /**
     * Represents prefix to logged finest
     */
    protected static String FINEST_PREFIX = null;

    /**
     * Cached TopLink prefix string.
     */
    protected static String TOPLINK_PREFIX = null;

    /**
     * Connection string
     */
    protected static final String CONNECTION_STRING = "Connection";

    /**
     * Thread string
     */
    protected static final String THREAD_STRING = "Thread";

    /**
     * Represents the writer that will receive the formatted log entries
     */
    protected Writer writer;

    /**
     * The pattern for formatting date-time in the log entries.
     * By default, set to {@code "yyyy.MM.dd HH:mm:ss.SSS"}.
     */
    protected static String DATE_FORMAT_STR = "yyyy.MM.dd HH:mm:ss.SSS";

    /**
     * Formatter used to print the current date/time.
     */
    protected DateTimeFormatter timeStampFormatter;

    /*
     * Allows the printing of the stack to be explicitly disabled/enabled.
     * CR #3870467.
     * null value is default behavior of determining from log level.
     */
    private Boolean shouldLogExceptionStackTrace;

    /*
     * Allows the printing of the date to be explicitly disabled/enabled.
     * CR #3870467.
     * null value is default behavior of determining from log level.
     */
    private Boolean shouldPrintDate;

    /*
     * Allows the printing of the thread to be explicitly disabled/enabled.
     * CR #3870467.
     * null value is default behavior of determining from log level.
     */
    private Boolean shouldPrintThread;

    /*
     * Allows the printing of the session to be explicitly disabled/enabled.
     * CR #3870467.
     * null value is default behavior of determining from log level.
     */
    private Boolean shouldPrintSession;

    /*
     * Allows the printing of the connection to be explicitly disabled/enabled.
     * CR #4157545.
     * null value is default behavior of determining from log level.
     */
    private Boolean shouldPrintConnection;

    /*
     * Used to determine if bind parameters should be logged or hidden.
     */
    private Boolean shouldDisplayData;

    /**
     * Return the system default log level property value.
     *
     * @return the system default log level property value or {@code null} if no such property is set.
     */
    private static String getDefaultLoggingLevelProperty() {
        if (System.getSecurityManager() != null) {
            return AccessController.doPrivileged(
                    (PrivilegedAction<String>) () -> System.getProperty(LOGGING_LEVEL, null));
        } else {
            return System.getProperty(LOGGING_LEVEL, null);
        }
    }

    /**
     * Return the system default log level.
     * This is based on the System property "eclipselink.logging.level", or INFO if not set.
     *
     * @return the system default log level
     */
    public static int getDefaultLoggingLevel() {
        return translateStringToLoggingLevel(getDefaultLoggingLevelProperty());
    }

    /**
     * Creates a new instance of {@linkplain AbstractSessionLog} class.
     */
    protected AbstractSessionLog() {
        this.writer = new PrintWriter(System.out);
        this.level = getDefaultLoggingLevel();
    }

    /**
     * Return the log level.
     * It is used when session is not available.
     *
     * @return the log level
     */
    @Override
    public int getLevel() {
        return getLevel(null);
    }

    /**
     * Return the log level as a string value.
     */
    @Override
    public String getLevelString() {
        return LogLevel.toValue(getLevel(), LogLevel.INFO).getName();
    }

    /**
     * Return the log level for the category name space.
     *
     * @return the log level
     * @param category  the string representation of a EclipseLink category, e.g. "sql", "transaction" ...
     */
    @Override
    public int getLevel(String category) {
        return level;
    }

    /**
     * Set the log level.  It is used when session is not available.
     *
     * @param level     the new log level
     */
    @Override
    public void setLevel(int level) {
        setLevel(level, null);
    }

    /**
     * Set the log level for the category name space.
     *
     * @param level     the new log level
     * @param category  the string representation of an EclipseLink category, e.g. "sql", "transaction" ...
     */
    @Override
    public void setLevel(int level, String category) {
        this.level = level;
    }

    /**
     * Check if a message of the given level would actually be logged.
     * It is used when session is not available.
     *
     * @return true if the given message level will be logged
     * @param level  the log request level
     */
    @Override
    public boolean shouldLog(int level) {
        return shouldLog(level, null);
    }

    /**
     * Check if a message of the given level would actually be logged for the category name space.
     * !isOff() is checked to screen out the possibility when both
     * log level and log request level are set to OFF.
     *
     * @return true if the given message level will be logged
     * @param level  the log request level
     * @param category  the string representation of an EclipseLink category, e.g. "sql", "transaction" ...*
     */
    @Override
    public boolean shouldLog(int level, String category) {
        return (this.level <= level) && !isOff();
    }

    /**
     * Return the singleton SessionLog.  If the singleton SessionLog does not exist,
     * a new one is created based on the version of JDK being used from the Version class.
     *
     * @return the singleton SessionLog
     */
    public static SessionLog getLog() {
        if (defaultLog == null) {
            defaultLog = new DefaultSessionLog();
        }
        return defaultLog;
    }

    /**
     * Set the singleton SessionLog.
     *
     * @param sessionLog  a SessionLog
     */
    public static void setLog(SessionLog sessionLog) {
        defaultLog = sessionLog;
        defaultLog.setSessionName(null);
    }

    @Override
    public String getSessionName() {
        return sessionName;
    }

    @Override
    public void setSessionName(String sessionName) {
        this.sessionName = sessionName;
    }

    @Override
    public void log(int level, Supplier<String> messageSupplier) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, messageSupplier);
    }

    @Override
    public void log(int level, String category, Supplier<String> messageSupplier) {
        if (!shouldLog(level, category)) {
            return;
        }
        log(new SessionLogEntry(level, category, null, messageSupplier.get(), null, null, false));
    }

    @Override
    public void log(int level, String message) {
        // Target method has shouldLog check, so it's skipped here.
        // Warning: do not use this function to pass in bundle keys as they will not get transformed into string messages
        //Bug#4566524  Pass in false for external use
        log(level, null, message, null, false);
    }

    @Override
    public void log(int level, String message, Object param) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, new Object[] { param }, true);
    }

    @Override
    public void log(int level, String category, String message, Object param) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, category, message, new Object[] { param }, true);
    }

    @Override
    public void log(int level, String message, Object param1, Object param2) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, new Object[] { param1, param2 }, true);
    }

    @Override
    public void log(int level, String category, String message, Object param1, Object param2) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, category, message, new Object[] { param1, param2 }, true);
    }

    @Override
    public void log(int level, String message, Object param1, Object param2, Object param3) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, new Object[] { param1, param2, param3 }, true);
    }

    @Override
    public void log(int level, String category, String message, Object param1, Object param2, Object param3) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, category, message, new Object[] { param1, param2, param3 }, true);
    }

    @Override
    public void log(int level, String message, Object param1, Object param2, Object param3, Object param4) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, new Object[] { param1, param2, param3, param4 }, true);
    }

    @Override
    public void log(int level, String category, String message, Object param1, Object param2, Object param3, Object param4) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, category, message, new Object[] { param1, param2, param3, param4 }, true);
    }

    @Override
    public void log(int level, String message, Object[] params) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, params, true);
    }

    @Override
    public void log(int level, String category, String message, Object[] params) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, category, message, params, true);
    }

    @Override
    @Deprecated(forRemoval=true, since="4.0.9")
    public void log(int level, String message, Object[] params, boolean shouldTranslate) {
        // Target method has shouldLog check, so it's skipped here.
        log(level, null, message, params, shouldTranslate);
    }

    @Override
    @Deprecated(forRemoval=true, since="4.0.9")
    public void log(int level, String category, String message, Object[] params, boolean shouldTranslate) {
        if (!shouldLog(level, category)) {
            return;
        }
        log(new SessionLogEntry(level, category, null, message, params, null, shouldTranslate));
    }

    // Remove with deprecated API removal
    // Internal shortcut for translated message (public API method has translation turned off)
    private void logTranslated(int level, String message) {
        log(level, null, message, null, true);
    }

    /**
     * Whether the session should be printed as part of the log messages.
     * By default, the session is always printed, but can be explicitly turned off or on.
     *
     * @return value of {@code true} when the session should be printed as part of the log messages
     *         or {@code false} otherwise
     */
    @Override
    public boolean shouldPrintSession() {
        return shouldPrintSession == null || shouldPrintSession;
    }

    /**
     * Turn printing of the session as part of the log messages on or off explicitly.
     *
     * @param shouldPrintSession value of {@code true} when the session should be printed
     *                           as part of the log messages or {@code false} otherwise
     */
    @Override
    public void setShouldPrintSession(boolean shouldPrintSession) {
        this.shouldPrintSession = shouldPrintSession;
    }

    /**
     * Whether the connection should be printed as part of the log messages.
     * By default, the connection is always printed when available, but can be explicitly turned off or on.
     *
     * @return value of {@code true} when the connection should be printed as part of the log messages
     *         or {@code false} otherwise
     */
    @Override
    public boolean shouldPrintConnection() {
        return shouldPrintConnection == null || shouldPrintConnection;
    }

    /**
     * Turn printing of the connection as part of the log messages on or off explicitly.
     *
     * @param shouldPrintConnection value of {@code true} when the connection should be printed
     *                              as part of the log messages or {@code false} otherwise
     */
    @Override
    public void setShouldPrintConnection(boolean shouldPrintConnection) {
        this.shouldPrintConnection = shouldPrintConnection;
    }

    /**
     * Whether the {@linkplain Exception} stack trace should be logged.
     * By default, the stack is logged for FINER or less (finest). The logging of the stack
     * can be explicitly turned on or off.
     *
     * @return value of {@code true} when the {@linkplain Exception} stack trace should be logged
     * or {@code false} otherwise
     */
    @Override
    public boolean shouldLogExceptionStackTrace() {
        return shouldLogExceptionStackTrace == null
                ? getLevel() <= FINER
                : shouldLogExceptionStackTrace;
    }

    /**
     * Turn {@linkplain Exception} stack trace logging on or off explicitly.
     *
     * @param shouldLogExceptionStackTrace value of {@code true} when the {@linkplain Exception} stack trace
     *                                     should be logged or {@code false} otherwise
     */
    @Override
    public void setShouldLogExceptionStackTrace(boolean shouldLogExceptionStackTrace) {
        this.shouldLogExceptionStackTrace = shouldLogExceptionStackTrace;
    }

    /**
     * Whether the SQL logging should log visible the bind parameters.
     * By default, the bind parameters are printed for FINE or less (finer, etc.). The printing of the bind
     * parameters can be explicitly turned on or off.
     *
     * @return value of {@code true} when the bind parameters should be printed as part of the SQL log messages
     *         or {@code false} otherwise
     */
    @Override
    public boolean shouldDisplayData() {
        return this.shouldDisplayData == null
                ? getLevel() <= FINE
                : shouldDisplayData;
    }

    /**
     * Turn printing of the bind parameters as part of the SQL log messages on or off explicitly.
     *
     * @param shouldDisplayData value of {@code true} when the bind parameters should be printed
     *                          as part of the SQL log messages or {@code false} otherwise
     */
    @Override
    public void setShouldDisplayData(Boolean shouldDisplayData) {
        this.shouldDisplayData = shouldDisplayData;
    }

    /**
     * Whether the date should be printed as part of the log messages.
     * By default, the date is always printed, but can be explicitly turned off or on.
     *
     * @return value of {@code true} when the date should be printed as part of the log messages
     *         or {@code false} otherwise
     */
    @Override
    public boolean shouldPrintDate() {
        return shouldPrintDate == null || shouldPrintDate;
    }

    /**
     * Turn printing of the date as part of the log messages on or off explicitly.
     *
     * @param shouldPrintDate value of {@code true} when the date should be printed as part
     *                        of the log messages or {@code false} otherwise
     */
    @Override
    public void setShouldPrintDate(boolean shouldPrintDate) {
        this.shouldPrintDate = shouldPrintDate;
    }

    /**
     * Whether the thread should be printed as part of the log messages.
     * By default, the thread is printed for FINE or less (finer, etc.). The printing of the thread
     * can be explicitly turned on or off.
     *
     * @return value of {@code true} when the thread should be printed as part of the log messages
     *         or {@code false} otherwise
     */
    @Override
    public boolean shouldPrintThread() {
        return shouldPrintThread == null
                ? getLevel() <= FINE
                : shouldPrintThread;
    }

    /**
     * Turn printing of the thread as part of the log messages on or off explicitly.
     *
     * @param shouldPrintThread value of {@code true} when the thread should be printed
     *                          as part of the log messages or {@code false} otherwise
     */
    @Override
    public void setShouldPrintThread(boolean shouldPrintThread) {
        this.shouldPrintThread = shouldPrintThread;
    }

    /**
     * Return the writer that will receive the formatted log entries.
     *
     * @return the log writer
     */
    @Override
    public Writer getWriter() {
        return writer;
    }

    /**
     * Set the writer that will receive the formatted log entries.
     *
     * @param writer  the log writer
     */
    @Override
    public void setWriter(Writer writer) {
        this.writer = writer;
    }


    /**
     * Set the writer that will receive the formatted log entries.
     *
     * @param outputstream  the log writer
     */
    public void setWriter(OutputStream outputstream) {
        this.writer = new OutputStreamWriter(outputstream);
    }

    /**
     * Return the specified date and/or time information in string.
     * The format will be determined by the {@linkplain  DateTimeFormatter} settings.
     * By default, the value of the {@linkplain #DATE_FORMAT_STR} pattern is used.
     */
    public DateTimeFormatter getTimeStampFormatter() {
        if (timeStampFormatter == null) {
            timeStampFormatter = DateTimeFormatter
                    .ofPattern(DATE_FORMAT_STR)
                    .withZone(ZoneId.systemDefault());
        }
        return timeStampFormatter;
    }

    protected String getTimeStampString(TemporalAccessor temporalAccessor) {
        if (temporalAccessor == null) {
            return null;
        }
        return getTimeStampFormatter().format(temporalAccessor);
    }

    /**
     * Return the supplement detail information including date, session, thread, connection,
     * source class name and source method name.
     */
    protected String getSupplementDetailString(SessionLogEntry entry) {
        StringWriter writer = new StringWriter();

        if (shouldPrintDate()) {
            writer.write(getTimeStampString(entry.getTimeStamp()));
            writer.write("--");
        }
        if (shouldPrintSession() && entry.getSessionId() != null) {
            writer.write(entry.getSessionId());
            writer.write("--");
        }
        if (shouldPrintConnection() && entry.getConnectionId() != null) {
            writer.write(this.getConnectionString(entry.getConnectionId()));
            writer.write("--");
        }
        if (shouldPrintThread()) {
            writer.write(this.getThreadString(entry.getThread()));
            writer.write("--");
        }
        if (entry.getSourceClassName() != null) {
            writer.write(entry.getSourceClassName());
            writer.write("--");
        }
        if (entry.getSourceMethodName() != null) {
            writer.write(entry.getSourceMethodName());
            writer.write("--");
        }
        return writer.toString();
    }

    /**
     * Return the specified connection information.
     *
     * @param connectionId the identifier of the datasource connection
     * @return connection string to be printed to the logs
     */
    protected String getConnectionString(int connectionId) {
        return CONNECTION_STRING + "(" + connectionId + ")";
    }

    /**
     * Return the specified thread information.
     */
    protected String getThreadString(Thread thread) {
        return THREAD_STRING + "(" + thread + ")";
    }

    /**
     * Print the prefix string representing EclipseLink logging
     */
    //Bug3135111  Prefix strings are not translated until the first time they are used.
    protected void printPrefixString(int level, String category) {
        try {
                this.getWriter().write(getPrefixString(level, category));
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    /**
     * Get the prefix string for the EclipseLink logging
     */
    protected String getPrefixString(int level, String category) {
        StringBuilder sb = new StringBuilder();
        switch (level) {
            case SEVERE:
                if (SEVERE_PREFIX == null) {
                    SEVERE_PREFIX = LoggingLocalization.buildMessage("toplink_severe");
                }
                sb.append(SEVERE_PREFIX);
                break;
            case WARNING:
                if (WARNING_PREFIX == null) {
                    WARNING_PREFIX = LoggingLocalization.buildMessage("toplink_warning");
                }
                sb.append(WARNING_PREFIX);
                break;
            case INFO:
                if (INFO_PREFIX == null) {
                    INFO_PREFIX = LoggingLocalization.buildMessage("toplink_info");
                }
                sb.append(INFO_PREFIX);
                break;
            case CONFIG:
                if (CONFIG_PREFIX == null) {
                    CONFIG_PREFIX = LoggingLocalization.buildMessage("toplink_config");
                }
                sb.append(CONFIG_PREFIX);
                break;
            case FINE:
                if (FINE_PREFIX == null) {
                    FINE_PREFIX = LoggingLocalization.buildMessage("toplink_fine");
                }
                sb.append(FINE_PREFIX);
                break;
            case FINER:
                if (FINER_PREFIX == null) {
                    FINER_PREFIX = LoggingLocalization.buildMessage("toplink_finer");
                }
                sb.append(FINER_PREFIX);
                break;
            case FINEST:
                if (FINEST_PREFIX == null) {
                    FINEST_PREFIX = LoggingLocalization.buildMessage("toplink_finest");
                }
                sb.append(FINEST_PREFIX);
                break;
            default:
                if (TOPLINK_PREFIX == null) {
                    TOPLINK_PREFIX = LoggingLocalization.buildMessage("toplink");
                }
                sb.append(TOPLINK_PREFIX);
        }
        if (category != null) {
            sb.append(category);
            sb.append(": ");
        }
        return sb.toString();
    }

    /**
     * Set the date-time format to be used when printing a log entry date.
     *
     * @param timeStampFormatter Formatter for printing time stamp in the log entry.
     */
    public void setTimeStampFormatter(DateTimeFormatter timeStampFormatter) {
        this.timeStampFormatter = timeStampFormatter;
    }

    /**
     * Return the formatted message based on the information from the given SessionLogEntry.
     * The message will either be translated and formatted or formatted only depending
     * on if the shouldTranslate flag is set to true of false.
     */
    protected String formatMessage(SessionLogEntry entry) {
        String message = entry.getMessage();
        if (entry.shouldTranslate()) {
            if (entry.getLevel() > FINE) {
                message = LoggingLocalization.buildMessage(message, entry.getParameters());
            } else {
                message = TraceLocalization.buildMessage(message, entry.getParameters(), true);
            }
        } else {
            //Bug5976657, if there are entry parameters and the string "{0" contained in the message
            //body, we assume it needs to be formatted.
            if (entry.getParameters() != null && entry.getParameters().length > 0 && message.contains("{0")) {
                message = MessageFormat.format(message, entry.getParameters());
            }
        }
        return message;
    }

    /**
     * Translate the string value of the log level to the constant value.
     * If value is {@code null} or invalid use the default.
     *
     * @return the constant value of the provided log level {@linkplain String}
     */
    public static int translateStringToLoggingLevel(String loggingLevel) {
        final LogLevel logLevel = LogLevel.toValue(loggingLevel);
        return logLevel != null ? logLevel.getId() : LogLevel.INFO.getId();
    }

    /**
     * Log a throwable at FINER level.
     *
     * @param throwable a Throwable
     */
    @Override
    public void throwing(Throwable throwable) {
        if (shouldLog(FINER)) {
            log(new SessionLogEntry(FINER, null, null, "", throwable));
        }
    }

    @Override
    public void severe(String message) {
        logTranslated(SEVERE, message);
    }

    @Override
    public void severe(Supplier<String> messageSupplier) {
        log(SEVERE, messageSupplier);
    }

    @Override
    public void warning(String message) {
        logTranslated(WARNING, message);
    }

    @Override
    public void warning(Supplier<String> messageSupplier) {
        log(WARNING, messageSupplier);
    }

    @Override
    public void info(String message) {
        logTranslated(INFO, message);
    }

    @Override
    public void info(Supplier<String> messageSupplier) {
        log(INFO, messageSupplier);
    }

    @Override
    public void config(String message) {
        logTranslated(CONFIG, message);
    }

    @Override
    public void config(Supplier<String> messageSupplier) {
        log(CONFIG, messageSupplier);
    }

    @Override
    public void fine(String message) {
        logTranslated(FINE, message);
    }

    @Override
    public void fine(Supplier<String> messageSupplier) {
        log(FINE, messageSupplier);
    }

    @Override
    public void finer(String message) {
        logTranslated(FINER, message);
    }

    @Override
    public void finer(Supplier<String> messageSupplier) {
        log(FINER, messageSupplier);
    }

    @Override
    public void finest(String message) {
        logTranslated(FINEST, message);
    }

    @Override
    public void finest(Supplier<String> messageSupplier) {
        log(FINEST, messageSupplier);
    }

    /**
     * Log a throwable with level.
     *
     * @param level  the log request level value
     * @param throwable  a Throwable
     */
    @Override
    public void logThrowable(int level, Throwable throwable) {
        // Must not create the log if not logging as is a performance issue.
        if (shouldLog(level)) {
            log(new SessionLogEntry(level, null, null, "", throwable));
        }
    }

    /**
     * Log a throwable with level.
     *
     * @param level  the log request level value
     * @param throwable  a Throwable
     */
    @Override
    public void logThrowable(int level, String category, Throwable throwable) {
        // Must not create the log if not logging as is a performance issue.
        if (shouldLog(level, category)) {
            log(new SessionLogEntry(level, category, null, "", throwable));
        }
    }

    /**
     * INTERNAL:
     * Check if the log level is set to off.
     */
    public boolean isOff() {
        return this.level == OFF;
    }

    /**
     * INTERNAL:
     * Each session owns its own session log because session is stored in the session log
     */
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (Exception exception) {
            throw new AssertionError(exception);
        }
    }

}
